查看原文
其他

在Unity中实现程序化音频

Unity官方 Unity官方平台 2019-05-08

游戏的音频在很大的程度上可以影响游戏体验,所以在游戏开发中我们不得忽视音频开发的重要性。例如休闲小游戏《Rocket Plume》中,玩家利用火箭喷出的像素流来清理岩石,清理岩石的频繁音效一旦修饰不佳,容易让人感觉枯燥无味,从而带来不好的体验。


本文将由这款休闲小游戏的作者Joe Strout为大家分享他们在音频开发时遇到的问题和解决方案。下面是Joe Strout带来的分享内容。


音频开发问题


在开发《Rocket Plume》的过程中,音频方面主要遇到了两大难题。


第一个问题和火箭引擎发出的声音有关。在玩家游戏的大部分时间里引擎都是启动状态,无论怎样尝试着循环音频,玩家都能分辨出这是一段被反复播放的音频。这就会让游戏的声音听起来不太自然,分散玩家在游戏上的注意力。


第二个问题,也是最难搞定的问题,和游戏机制有关。在这款游戏的大部分时间里,玩家利用火箭喷出的像素流来清理岩石,且游戏中会非常频繁地重复这一过程,有时甚至会在一帧中摧毁多个像素的岩石。即便是用非常短的数字化声音,每摧毁一个像素就播放一次的话也会非常抓耳,让人感到枯燥乏味。我们也尝试了声音的各种修饰,使用随机声调和音量等,但效果仍然不理想。


程序化音频


对于《Rocket Plume》音频开发的两大难题,可行的解决方案就是程序化音频(Procedural Audio)。这是一种根据需求使用代码生成声音波形的方法。利用程序化音频能够使创造出永不重复的音频成为可能,同样也可以对游戏内发生的事情产生及时的反馈。


幸运的是,Unity支持直接在音频处理流中插入代码,这一鲜为人知但简单易用的方法使得程序化音频实现非常便捷。



Unity中的声音来源于Audio Source组件,并通过GameObject上的一个或多个Audio Filter组件修饰。这些滤波器组件按照组件列表中的顺序应用到声音中。Unity内置的滤波器组件包含高通及低通滤波器、回声(Echo)、畸变(Distortion)、混响(Reverb)和合唱(Chorus)效果。


如何创建程序化音频


创建程序化音频,只需新建一个MonoBehaviour子类,并实现OnAudioFilterRead方法。这样就实现了一个自定义音频滤波器。您可以根据喜好在音频处理链中加入滤波器。需要特别说明的是,要想凭空创造出声音,必须将您的音频滤波器放在音频处理栈中的音源组件之后、,所有其它音频滤波器之前。如果Audio Source组件不包含任何音频组件,该组件就会向滤波器链中直接输入零值,也就是说没有声音,而这就是依靠代码发声的原理。


多说无益,下面来看一些例子。


案例一


首先来解决第一个问题:引擎的噪音。和很多自然界中的声音一样,引擎噪音本质上就是被某些滤波器修饰后的白噪音——即完全随机的声波。因此第一个程序化音频脚本非常简单,它基本上就是用来生成白噪音的。


using UnityEngine;
public class EngineAudio : MonoBehaviour {
    
    [Range(-1f, 1f)]
    public float offset;
    
    System.Random rand = new System.Random();
    
    void OnAudioFilterRead(float[] data, int channels) {
        for (int i = 0; i < data.Length; i++) {
            data[i] = (float)(rand.NextDouble() * 2.0 - 1.0 + offset);
        }
    }
}


神奇的OnAudioFilterRead方法的接收参数是一个浮点数数组data和正在使用的频道数量channels。数组表示波形,以交叉格式存储:首先是各个频道的第一个样本,然后是各个频道的第二个样本,以此类推。该方法会被频繁调用(差不多是每20毫秒一次),同时还需要适当大小的数据缓存。每个数据采样的取值范围是-1到1,不在范围内的数值均忽略不计。

 

由于是在所有的频道生成白噪音,因此不需要关注这些细节,只需在每次采样时向缓冲区中填一个随机数就好了。这里对白噪音做了一点点偏移处理,也算是一种非常简单的音频滤波器,这样做可以避免采样的某些片段越界。

 

将该脚本添加到一个带有AudioSource组件的GameObject上,然后运行游戏,您会听到刺耳的“静电声”。适当调整偏移量,可以让声音缓和一点。但我们并不想让玩家听到这样的声音,还需要添加更多的音频滤波器。我们还使用了音频低通滤波器,将截止频率(cutoff frequency)设置为800左右,Q参数设置为1。接着使用音频失真滤波器,并将失真水平(distortion level)设置为0.5。这样就得到了一种平滑丰富且听起来非常像引擎的声音。因为这种声音是即时生成的,所以它不会让人感到重复单调。


注意,以上代码是简单的范例,实际上《Rocket Plume》游戏中不是直接使用。因为直接打开或关闭(例如启用或禁用这个GameObject)这个引擎噪音带来的体验不是太好,对比引擎关闭后的寂静,引擎再次开启后简直震耳欲聋,尤其是在玩家关掉了背景音乐的情况下。

 

最后我们决定不完全关闭引擎的噪音,而是在引擎“空闲”时使用一种音量很低的背景音。测试过一些音频滤波器后,我们发现减少低通滤波频率可以达到这种效果,然后将这种机制与引擎的开关挂钩。我们对此有过一些争论,即这些脚本是否和白噪音生成器属于相同类型的程序,不过最后我们还是决定把所有引擎相关的代码保存到一起。


最终的脚本如下所示:

using UnityEngine;

public class EngineAudio : MonoBehaviour {
    
    [Range(-1f, 1f)]
    public float offset;
    
    public float cutoffOn = 800;
    public float cutoffOff = 100;
    
    public bool engineOn;
   
    System.Random rand = new System.Random();
    AudioLowPassFilter lowPassFilter;
    
    void Awake() {
        lowPassFilter = GetComponent<AudioLowPassFilter>();
        Update();
    }
    
    void OnAudioFilterRead(float[] data, int channels) {
        for (int i = 0; i < data.Length; i++) {
            data[i] = (float)(rand.NextDouble() * 2.0 - 1.0 + offset);
        }
    }
    
    void Update() {
        lowPassFilter.cutoffFrequency = engineOn ? cutoffOn : cutoffOff;
    }
}


案例二



接下来我们来看一下第二个问题,这个问题是关于岩石破裂音的,当火箭尾流消解掉岩石时就会发出这样的声音。这种声音不仅仅是用来取悦玩家耳朵的,它也是一种重要的反馈,能告知玩家发生了什么事情,例如了解何时打开了通路可以继续前进了。再次强调,白噪音是声音系统的核心。这次只想在像素被炸开时发出一点点轻微的白噪音。我们称这些爆炸声为“click”,这些声音听起来是互相独立的,没有经过任何过滤。这里的音频脚本带有一个点击计数器,当像素被摧毁时这个计数器就会增长(增长操作由其他代码负责),然后滤波器代码负责消耗掉这些计数并生成“click”声,这也可以保证生成的“click”声的数量总是与消除像素数量一致。


脚本代码如下:

using UnityEngine;
public class RockChewAudio : MonoBehaviour {
    
    public static int clicks = 0;
    System.Random rand = new System.Random();
        
    void OnAudioFilterRead(float[] data, int channels) {
        bool inClick = false;    // 我们是否生成了 click 或 silence
        int samplesLeft = 0;    // 我们需要历经多少 click 或 silence
        for (int i = 0; i < data.Length; i += channels) {
            if (samplesLeft < 1) {
                // If out of clicks, then just generate silence for the rest of the time.
                if (clicks < 1) {
                    inClick = false;
                    samplesLeft = data.Length / channels;
                } else if (inClick) {
                    // Generate a small random silence.
                    inClick = false;
                    samplesLeft = rand.Next(1,10);
                } else {
                    // Generate a click.
                    inClick = true;
                    samplesLeft = rand.Next(2,5);
                    clicks--;
                }
            }
            for (int j=0; j<channels; j++) {
                data[i+j] = inClick ? (float)(rand.NextDouble() * 2.0 - 1.0) : 0;
            }            
            samplesLeft--;
        }
        clicks = 0;
    } 
}


这里的for主循环稍稍有些复杂, 因为牵涉到了频道。不过基本思路还是一样的:迭代所有的采样样本,生成“click”音(白噪音),或不生成声音。当前阶段的“click”片段或静音片段的所有样本生成完毕后,再继续生成下一段。所有需要的“click”生成完毕后,让缓冲区中剩余的部分保持静音即可。


和引擎噪音一样,这里也使用了低通和失真音频滤波器。低通截止值要高一些(3000左右)。


最终的结果是,岩石爆炸时听起来更像是基本粒子互相碰撞融合的声音。不过再怎么去描述也不如亲耳去听一下,查看下方游戏视频,您可以听到所有的最终效果。


https://v.qq.com/txp/iframe/player.html?vid=n03839470xt&width=500&height=375&auto=0

总结


希望这篇文章能够给大家一个清晰的认识,即在Unity中制作出这样的程序化音频是非常容易的。放开自己的想象力,摆脱掉那些单调乏味的声音效果,考虑为您的游戏加入一些这样的程序化音效吧。如果您想了解更多Unity中音频有关的内容,请点击【阅读原文】访问Unity官方中文社区(forum.china.unity3d.com)!


更多Unity相关技术文章

Playable API:动画和视频进入新阶段

Unity崩溃报告自动处理工具:Crash Analyzer

Amplify Shader Editor基础教程

如何在Unity中实现非真实感渲染

使用Unity进行增强现实中的光照和阴影的渲染


Unite 2017 Shanghai

Unite 2017 Shanghai将于5月11 - 13日在上海国际会议中心举行。5折个人通票开售赞助商招募已开启,愿您与我们一同打造一场Unity开发者盛会!Made with Unity展区作品征集等更多信息请访问Unite 2017 Shanghai官方网站(unite2017.csdn.net)!



点击“阅读原文”访问Unity官方中文社区!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存